package pt.ua.miect.sd.simuladorpesca.entities;

import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.Semaphore;
import org.apache.log4j.Logger;
import pt.ua.miect.sd.simuladorpesca.SimulationConfig;

public class Map {

    private static Map singleton;
    private ConcurrentHashMap<String, MapEntity> mapEntities;
    private ConcurrentHashMap<MapPosition, FishSchool> underwater;
    private ConcurrentHashMap<MapPosition, CopyOnWriteArrayList<Boat>> abovewater;
    private ConcurrentHashMap<MapPosition, CopyOnWriteArrayList<FishNet>> fishNets;
    private LinkedBlockingQueue<MapPosition> updatedElements; //stores elements that have ben changed on map, used to update the ui.
    private Port port;
    private Semaphore semaphore;

    public synchronized static Map getInstance() {
        if (singleton == null) {
            singleton = new Map();
        }
        return singleton;
    }

    private Map() {
        this.mapEntities = new ConcurrentHashMap<String, MapEntity>();
        this.underwater = new ConcurrentHashMap<MapPosition, FishSchool>();
        this.fishNets = new ConcurrentHashMap<MapPosition, CopyOnWriteArrayList<FishNet>>();
        this.abovewater = new ConcurrentHashMap<MapPosition, CopyOnWriteArrayList<Boat>>();
        this.updatedElements = new LinkedBlockingQueue<MapPosition>();
        semaphore = new Semaphore(5);
        port = new Port();
    }

    /**
     *  Adds a new element to the map
     * @param key
     * @param entitie
     */
    public synchronized void addEntity(String key, MapEntity entitie) {
        mapEntities.put(key, entitie);
        if (entitie instanceof FishSchool) {
            FishSchool fcluster = (FishSchool) entitie;
            while (underwater.containsKey(fcluster.position) || fcluster.position.equals(0, 0)) {
                fcluster.setMapPosition(MapPosition.genRandomPosition());
            }
            underwater.put(fcluster.position, fcluster);

        }
    }

    /*
     * testes if a fishschool can perform a move, if true makes the move and return true, else return false;
     */
    public boolean allowMove(FishSchool targetFish, MapPosition targetPosition) {
        boolean move = false;
        try {
            semaphore.acquire();
            if (targetFish.getStatus() != FishSchool.FishSchoolState.TRAPPED_IN_NET) {
                if (!underwater.containsKey(targetPosition)) //there no are other fish schools on the target position
                {
                    underwater.remove(targetFish.getMapPosition());//removes fish school from  current position
                    targetFish.setMapPosition(targetPosition);//sets new position
                    underwater.put(targetPosition, targetFish);//adds to hashmap
                    move = true;
                }
            }
            semaphore.release();
        } catch (InterruptedException ex) {
            Logger.getLogger(Map.class).error(ex);
        }
        return move;


    }

    public MapEntity getEntity(String id) {
        if (mapEntities.containsKey(id)) {
            return mapEntities.get(id);

        }

        return null;
    }

    public synchronized void SetFishSchoolStatus(String schoolId, FishSchool.FishSchoolState state) {
        FishSchool school = (FishSchool) getEntity(schoolId);
        if (school.getStatus() != FishSchool.FishSchoolState.TRAPPED_IN_NET) {
            school.setStatus(state);
        }
    }

    public boolean allowMove(Boat targetBoat, MapPosition targetPosition) {
        boolean move = true;
        try {
            semaphore.acquire();
            CopyOnWriteArrayList<Boat> list = abovewater.get(targetBoat.getMapPosition());
            if (list != null && list.size() > 2) {
                //has no empty space for a new boat
                move = false;
            } else {
                moveBoatToPosition(targetBoat, targetPosition);
                move = true;
            }
            semaphore.release();
        } catch (InterruptedException ex) {
            Logger.getLogger(Map.class).error(ex);
            return false;
        }
        return move;
    }

    /**
     * Casts a FishNet
     * @param boat
     * @param position
     */
    public void castFishNet(Boat boat, MapPosition position) {
        try {
            semaphore.acquire();
            if (!fishNets.containsKey(position)) {
                fishNets.put(position, new CopyOnWriteArrayList<FishNet>());
            }
            CopyOnWriteArrayList<FishNet> list = fishNets.get(position);
            FishNet newNet = new FishNet(boat);
            list.add(newNet);
            if (!list.isEmpty()) {
                FishNet companionNet = null;
                for (FishNet fishNet : list) {
                    if (fishNet.getCompany().equals(boat.getCompanyId())) {
                        companionNet = fishNet;
                        break;
                    }
                }
                FishSchool school = underwater.get(position);
                if (school != null && companionNet != null ) {
                    //calculating the catch
                    
                    //the catch can never be more than 220% the capacity of a Boat
                    long catchedFish = (long) (2.2 * SimulationConfig.MAX_BOAT_CAPACITY * Math.random());
                    if (catchedFish > school.getFishPopulation() + 2) {
                        //100% should not be possible
                        catchedFish = school.getFishPopulation() - 2;
                    }
                    school.setFishPopulation(school.getFishPopulation() - catchedFish);
                    school.setStatus(FishSchool.FishSchoolState.TRAPPED_IN_NET);
                    companionNet.setFishCount((long) catchedFish / 2);
                    newNet.setFishCount((long) catchedFish / 2);

                }
            }
            semaphore.release();
        } catch (InterruptedException ex) {
            Logger.getLogger(Map.class).error(ex);

        }

    }

    /**
     * Tracks fish on the sonar
     * @param targetPosition
     * @return
     */
    public MapPosition sonar(MapPosition targetPosition) {

        MapPosition sonarPing = null;
        try {
            semaphore.acquire();
            int[][] positions = {{0, 0}, {1, 0}, {-1, 0}, {0, 1}, {0, -1}};
            for (int pos = 0; pos < 5; pos++) {
                int newX = targetPosition.getXCoordinate() + positions[pos][0];
                int newY = targetPosition.getYCoordinate() + positions[pos][1];
                boolean valid = false;
                if (newX >= SimulationConfig.MAX_X_POSITION) {
                    valid = false;
                } else if (newX < 0) {
                    valid = false;
                } else if (newY >= SimulationConfig.MAX_Y_POSITION) {
                    valid = false;
                } else if (newY < 0) {
                    valid = false;
                } else if (newX == SimulationConfig.PORT_HOME_X && newY == SimulationConfig.PORT_HOME_Y) {
                    valid = false;
                } else {
                    //then it is a valid position
                    valid = true;
                }
                if (!valid) {
                    continue;
                }
                MapPosition scanPos = new MapPosition(newX, newY);
                FishSchool school = underwater.get(scanPos);
                if (school != null && school.getFishPopulation() > SimulationConfig.MINIMAL_FISH_COUNT) {
                    sonarPing = scanPos;
                    break;
                }

                semaphore.release();
            }
        } catch (InterruptedException ex) {
            Logger.getLogger(Map.class).error(ex);

        }

        return sonarPing;
    }

    private void moveBoatToPosition(Boat boat, MapPosition newPosition) {
        MapPosition currentPos = boat.position;

        CopyOnWriteArrayList<Boat> list = abovewater.get(currentPos);
        if (list != null) {
            list.remove(boat);
            if (list.isEmpty()) {
                //remove position from the hashmap
                abovewater.remove(currentPos, list);
            }
        }
        list = abovewater.get(newPosition);
        if (list == null) {
            list = new CopyOnWriteArrayList<Boat>();
            abovewater.put(newPosition, list);
        }

        list.add(boat);
        boat.setMapPosition(newPosition);

    }

    /**
     * @return the port
     */
    public synchronized Port getPort() {
        return port;
    }

    /**
     * Retrives the FishNet
     * @param boat
     * @param mapPosition
     * @return
     */
    public FishNet retrieveNet(Boat boat, MapPosition mapPosition) {
        FishNet net = null;
        try {
            semaphore.acquire();
            if (fishNets.containsKey(mapPosition)) {
                CopyOnWriteArrayList<FishNet> list = fishNets.get(mapPosition);
                for (FishNet fishNet : list) {
                    if (fishNet.getOwnerId().equals(boat.getId())) {
                        net = fishNet;
                        break;
                    }
                }
                if (net != null) {
                    list.remove(net);
                }
                FishSchool fish = underwater.get(mapPosition);
                if (fish != null) {
                    //calculate FishSchool State
                    if (fish.getStatus() == FishSchool.FishSchoolState.TRAPPED_IN_NET) {
                        if (list.size() < 2) {
                            fish.setStatus(FishSchool.FishSchoolState.FEEDING_AREA);
                        } else {
                            boolean sameCompany = false;
                            for (int i = 0; i < list.size(); i++) {
                                for (int j = i + 1; j < list.size(); j++) {
                                    if (list.get(j).getCompany().equals(list.get(i).getCompany())) {
                                        sameCompany = true;
                                        break;
                                    }
                                }
                                if (sameCompany) {
                                    break;
                                }
                            }
                            if (!sameCompany) {
                                //fish is free from the nets
                                fish.setStatus(FishSchool.FishSchoolState.FEEDING_AREA);
                            }
                        }
                    }
                }
                if (list.isEmpty()) {
                    fishNets.remove(mapPosition);
                }
            }
            semaphore.release();
        } catch (InterruptedException ex) {
            Logger.getLogger(Map.class).error(ex);
        }
        return net;
    }

    /**
     * Sonar that searches first on specific location (previous ping)
     * @param mapPosition
     * @param sonarPing
     * @return
     */
    public MapPosition sonar(MapPosition mapPosition, MapPosition sonarPing) {
        if (sonarPing!= null && sonarPing.getDistance(mapPosition) < 2 && underwater.containsKey(sonarPing)) {
            return sonarPing; //continues at the same location
        } else {
            return sonar(mapPosition);
        }
    }
}
